package org.spring.springboot.config.limit;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.alibaba.fastjson.JSON;
import com.google.common.util.concurrent.RateLimiter;



/**
 * @author xxw update 2020.3.26
 * 令牌桶算法--aop实现-api限流-单机-应该是使用信号量实现
 * 如果是分布式使用redis实现
 *
 * 特别说明 返回值head中 	LimitCache", "-1"返回数据为空 一般是第三方接口异常或者本服务异常
 * 						LimitCache", "0" 实时接口数据
 *						LimitCache", "1" 达到并发限制,缓存托底数据  
 *						LimitCache", "2" 达到并发限制,无缓存托底数据 返回空数据类型
 *
 *
 * 限制 取方法参数中第一个值,该值为对象,取对象多个参数联合作为缓存key
 *	
 *在注解处有两种模式 cache = 0 限流 不开启缓存
 *					cache = 1 限流 开启缓存
 *
 *对于令牌桶中令牌的产生一般有两种做法：

一种解法是，开启一个定时任务，由定时任务持续生成令牌。这样的问题在于会极大的消耗系统资源，
如，某接口需要分别对每个用户做访问频率限制，假设系统中存在6W用户，则至多需要开启6W个定时
任务来维持每个桶中的令牌数，这样的开销是巨大的。
另一种解法则是延迟计算，如上resync函数。该函数会在每次获取令牌之前调用，其实现思路为，若
当前时间晚于nextFreeTicketMicros，则计算该段时间内可以生成多少令牌，将生成的令牌加入令
牌桶中并更新数据。这样一来，只需要在获取令牌时计算一次即可。

 */
@Aspect
@Component
public class RequestRateLimitAspect {

	//线程池使用
	@Resource(name = "consumerQueueThreadPool")
	private ExecutorService consumerQueueThreadPool;
	
	@Autowired
	private RedisTemplate<Object, Object> jsonRedisTemplate;
	
	
	//配置个切入点
	@Pointcut("@annotation(com.yinzhilv.sbe.aspect.limit.RequestRateLimiter)")
	public void pointcut() {
	}
	
	private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();//不同的方法存放不同的令牌桶
	
	@Around("pointcut()")
	public Object around(ProceedingJoinPoint point) throws Throwable {
		HttpServletRequest request  = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
		HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
	    //获取切入点的注解
	    MethodSignature signature = (MethodSignature) point.getSignature();
	    Method method = signature.getMethod();
	    //返回类型
	    Class<?> returnType = method.getReturnType();
	    //获取切入点目标类、方法
	    String className = method.getDeclaringClass().getName();
	    String name = method.getName();
	    //类名和方法名作为key
	    String ipKey = String.format("%s#%s",className,name);
	    //获取方法的注解
	    RequestRateLimiter requestRateLimiter =  method.getAnnotation(RequestRateLimiter.class);
	    if (requestRateLimiter!=null) {//判断是否有注解
	    	response.setHeader("LimitCache", "0");//设置response是否使用缓存标记
	    	RateLimiter rateLimiter;
			if (!rateLimiterMap.containsKey(ipKey)) { // 判断map集合中是否有创建好的令牌桶 如果没
				rateLimiter = RateLimiter.create(requestRateLimiter.QPS());
				rateLimiterMap.put(ipKey, rateLimiter);
			}else {//如果有创建好的令牌桶
				rateLimiter = rateLimiterMap.get(ipKey);
			}
			//拿一个令牌去干活,如果没令牌了阻塞等待timeout毫秒后false
			boolean tryAcquire = rateLimiter.tryAcquire(requestRateLimiter.timeout(), requestRateLimiter.timeunit());
			System.out.println(tryAcquire+"令牌");
			if (tryAcquire) {//获取令牌成功
				Object proceed = point.proceed();
				if(proceed==null) {//返回结果为空，一般是异常了
					response.setHeader("LimitCache", "-1");//设置response是否使用缓存标记
					return returnType.newInstance();
				}
				if (requestRateLimiter.cache() == 1&&proceed!=null) {//当注解开启缓存和返回结果不为空
					String jsonStringProceed = JSON.toJSONString(proceed);//value
					String cacheKey = this.getCacheKey(point);//key
					long cacheTimeout = requestRateLimiter.cacheTimeout();//缓存时间
					this.setResponseToCache(cacheKey, jsonStringProceed, cacheTimeout);//异步执行缓存
				}
				return proceed;
			}else {//取不到令牌 即被限流
				if (requestRateLimiter.cache() == 1) {//启用 获取缓存托底数据
					response.setHeader("LimitCache", "1");//设置response是否使用缓存标记
					String cacheKey = this.getCacheKey(point);
					String responseFromCache = this.getResponseFromCache(cacheKey);
					if (responseFromCache ==null) {
						response.setHeader("LimitCache", "2");//设置response是否使用缓存标记
						return returnType.newInstance();//无缓存
					}
					Object parseObject = JSON.parseObject(responseFromCache, returnType);
					return parseObject;
				}else {//未启用缓存
					response.setHeader("LimitCache", "2");//设置response是否使用缓存标记
					return returnType.newInstance();
					
				}
			}
		}else {//如果没注解
			return point.proceed();
		}
		
		
	}
	// 缓存 异步
	public void setResponseToCache(String key,String value,long cacheTimeout) {
		Object object = jsonRedisTemplate.opsForValue().get(key);
		if (object==null) {
			CompletableFuture.runAsync(()->jsonRedisTemplate.opsForValue().set(key, value, cacheTimeout, TimeUnit.MILLISECONDS),consumerQueueThreadPool);
		}
	}
	// 获取 缓存
	public String getResponseFromCache(String key) {
		String object = (String) jsonRedisTemplate.opsForValue().get(key);
		return object;
	}
	//根据规则得到缓存key
	public String getCacheKey(ProceedingJoinPoint point) {
		Object[] args = point.getArgs();
		Object object = args[0];
		Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
	    RequestRateLimiter requestRateLimiter =  methodSignature.getMethod().getAnnotation(RequestRateLimiter.class);
	    Map<String, Object> keyAndValue = getKeyAndValue(object);
	    String cacheKey = Arrays.stream(requestRateLimiter.cacheKeyParameter().split(",")).map(t->String.valueOf(keyAndValue.get(t))).collect(Collectors.joining(","));
	    cacheKey = requestRateLimiter.cacheKeyName()+"_"+cacheKey;
		return cacheKey;
		
	}
	
	/**
     * 获取单个对象的所有键值对
     */
    public static <T> Map<String, Object> getKeyAndValue(T t) {
        Map<String, Object> map = new HashMap<String, Object>();
        Class clazz = (Class) t.getClass();
        Field[] fields = clazz.getDeclaredFields();
        map = Arrays.stream(fields).collect(Collectors.toMap(Field::getName, field -> {
            Object resultObj = null;
            field.setAccessible(true);
            try {
                resultObj = field.get(t);
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            return Optional.ofNullable(resultObj).orElse("");//当返回空的时候 让其为""
        }, (k1, k2) -> k2));//指定hash冲突的时候保留后来值
        return map;
    }

}
